package com.amadeus.spin.util.soapshooter;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;

import javax.activation.DataHandler;
import javax.mail.MessagingException;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.apache.soap.Constants;
import org.apache.soap.Fault;
import org.apache.soap.SOAPException;
import org.apache.soap.rpc.Call;
import org.apache.soap.rpc.Parameter;
import org.apache.soap.rpc.Response;
import org.apache.soap.transport.http.SOAPHTTPConnection;
import org.apache.soap.util.mime.ByteArrayDataSource;

import com.amadeus.spin.devtool.soap.shooter.config.ConfigurationParameters;
import com.amadeus.spin.standard.util.Dom4jUtils;
import com.amadeus.spin.standard.util.EncodingUtils;

/**
 * Facade class used to call our SOAP service without dealing with SOAP low
 * level aspects.<BR>
 * One instance of the class correspond to a given session and so should be
 * maintained from call to call.<BR>
 *
 * Soapclient is a library designed to easily send soap request to a soap
 * server.<BR>
 * The soap layer is hidden and thus different soap libraries (for instance
 * apache, axis or jaxm) can be used with no change in the code that rely on the
 * SoapClient.<BR>
 *
 * This version uses Apache Soap
 *
 * @version 0.2 <b>CR 02331331 AeTM - Enhancement of SOAP msg with a
 *          "without attachment" version</b> ( contact :
 *          dev.sep.dv2.arc.frw.oouattara )
 */
public class SoapClient implements Serializable, Shooter {

  /**
   * Our main service URN. Used as default.
   */
  final static String DEFAULT_SERVICE_URN = "urn:TripFlow";

  /**
   * Default server URL. Used to access a locally installed tunneling server.
   */
  final static String DEFAULT_SERVER_URL = "http://localhost:8080/SOAPRPCRouterServlet";

  /**
   * Default read timeout (ms). 0 indicates an infinite timeout.
   */
  final static int DEFAULT_TIMEOUT = 0;

  /**
   * Service method (with attached XML string).
   */
  final static String PROCESS = "processAttachment";

  private static Logger log = Logger.getLogger(SoapClient.class);

  private static final String SET_COOKIE_LOW = "Set-cookie";

  private static final String SET_COOKIE = "Set-Cookie";

  private static final long serialVersionUID = -2947964988689790807L;

  // SOAP target urn, saved as a workaround for a bug
  private String urnService = DEFAULT_SERVICE_URN;

  // SOAP router servlet url
  private URL servletUrl = null;

  private String requestUrl = null;

  // Cookie to be saved to maintain session in case of serialization
  private String sessionCookie = null;

  // SOAP connection object
  private SOAPHTTPConnection m_soapConnection = null;

  // SOAP action (not used right now)
  private String mSOAPAction = "";

  // indicates if we will use attachment or not
  private boolean useAttachment = true;

  // indicates if we will reuse cookies
  private boolean isSessionLess = false;

  // Timeout value
  private int sessionTimeout = DEFAULT_TIMEOUT;

  /**
   * Create a SoapClient object with default values :
   *
   * url=localhost:8080/SOAPRPCRouterServlet urn=urn:TripFlow timeout=0ms
   * (infinite)
   *
   */
  public SoapClient() throws SoapClientException {
    this(DEFAULT_SERVER_URL, DEFAULT_SERVICE_URN, DEFAULT_TIMEOUT);
  }

  /**
   * Create a SoapClient object with the given url and the default values :
   *
   * urn=urn:TripFlow timeout=0ms (infinite)
   *
   */
  public SoapClient(String url) throws SoapClientException {
    this(url, DEFAULT_SERVICE_URN, DEFAULT_TIMEOUT);
  }

  /**
   * Create a customised SoapClient object
   *
   * @param url
   *          server URL
   * @param urn
   *          service URN. To be used only with the apache soap library
   * @param timeout
   *          number of seconds to wait for answer (0 is infinite)
   */
  public SoapClient(String url, String urn, int timeout) throws SoapClientException {
    try {
      // Uses apache soap with attachement
      urnService = urn;
      servletUrl = new URL(url);
      requestUrl = url;
      sessionTimeout = timeout;
      m_soapConnection = new SOAPHTTPConnection();
      m_soapConnection.setMaintainSession(!isSessionLess);
      m_soapConnection.setTimeout(timeout);
    }
    catch (MalformedURLException e) {
      throw new SoapClientException("Soap configuration issue", e);
    }
  }

  /**
   * Create a customised SoapClient object
   *
   * @since 0.2
   *
   * @param url
   *          server URL
   * @param urn
   *          service URN. To be used only with the apache soap library
   * @param timeout
   *          number of seconds to wait for answer (0 is infinite)
   * @param useSoapAttachment
   *          Embbed the data in an attachement if true, in the body otherwise
   * @param soapActionTag
   *          value to set the SOAPAction tag, if null, then an empty string is
   *          used
   * @throws SoapClientException
   */
  public SoapClient(String url, String urn, int timeout, boolean useSoapAttachment, String soapActionTag)
      throws SoapClientException {
    this(url, urn, timeout);
    useAttachment = useSoapAttachment;
    if (null != soapActionTag) {
      mSOAPAction = soapActionTag;
    }
  }

  public void setUrn(String urn) {
    urnService = urn;
  }

  public void setTimeout(int timeout) {
    sessionTimeout = timeout;
    m_soapConnection.setTimeout(timeout);
  }

  /**
   * set proxy host and port
   *
   * @param proxy
   *          hostname
   * @param proxy
   *          port
   */
  public void setProxy(String host, int port) {
    m_soapConnection.setProxyHost(host);
    m_soapConnection.setProxyPort(port);
  }

  /**
   * setPassord method for proxy sets the password for basic HTTP authentication
   * through proxy
   *
   * @param proxy
   *          password
   */
  public void setProxyPassword(String password) {
    m_soapConnection.setProxyPassword(password);
  }

  /**
   * sets the user name for basic HTTP authentication through proxy
   *
   * @param proxy
   *          password
   */
  public void setProxyUserName(String uName) {
    m_soapConnection.setProxyUserName(uName);
  }

  /**
   * setPassord method sets the password for basic HTTP authentication
   *
   * @param password
   */
  public void setPassword(String password) {
    m_soapConnection.setPassword(password);
  }

  /**
   * setUserName method sets username for basic HTTP authentication
   *
   * @param username
   */
  public void setUserName(String username) {
    m_soapConnection.setUserName(username);

  }

  /**
   * Get the JSESSIONID Cookie (if available)
   *
   * @return The JSESSIONID cookie or null if not available
   */
  @Override
  public String getSessionCookie() {
    return sessionCookie;
  }

  /**
   * Send the request.
   *
   * @param request
   *          the request to be sent to the server
   * @return A String containing the reply of the server
   * @exception SoapClientException
   *              In case of a SOAP failure
   */
  @Override
  public String sendRequest(String request) throws SoapClientException {
    try {
      if (servletUrl == null) {
        throw new SOAPException(Constants.FAULT_CODE_CLIENT, "A URL must be specified during construction.");
      }
      // Configure SOAP call object :
      // Apache BUG / have to allocate a new Call object for each request
      // otherwise messages are growing (keep previous attachments).
      Call call = new Call();
      call.setTargetObjectURI(urnService);
      call.setEncodingStyleURI("http://schemas.xmlsoap.org/soap/encoding/");
      call.setSOAPTransport(m_soapConnection);
      call.setMethodName("process");
      String result = null;
      if (useAttachment) {
        result = processAttachment(call, request);
      }
      else {
        result = processPayload(call, request);
      }
      this.m_soapConnection.resetState();
      return result;
    }
    catch (SOAPException e) {
      throw new SoapClientException("SOAP Failure", e);
    }
  }

  /**
   * Invoke SOAP service. Send the request to the RPC service using attachment
   * (multipart body).
   */
  private String processAttachment(Call call, String xmlContent) throws SOAPException {
    List<Parameter> params = new ArrayList<Parameter>();
    String charset = getCharsetFromXml(xmlContent);
    ByteArrayDataSource ds = new ByteArrayDataSource(xmlContent, charset);
    params.add(new Parameter("message", javax.activation.DataHandler.class, new DataHandler(ds), null));
    call.setParams(new Vector<Parameter>(params));

    // Finally invoke the SOAP service on server
    Response resp = null;
    requestUrl = StringUtils.removeEnd(requestUrl, "/");
    // Build a special url containing the cookie
    URL tmpUrl = buildUrl();
    resp = call.invoke(tmpUrl, mSOAPAction);

    // Retrieves cookie if necessary
    if (sessionCookie == null) {
      Hashtable<String, String> ht = m_soapConnection.getHeaders();
      if (ht != null) {
        String cookie = ht.get(SET_COOKIE);
        if (cookie == null) {
          cookie = ht.get(SET_COOKIE_LOW);
        }
        if (cookie != null) {
          sessionCookie = cookie.split(";")[0];
        }
      }
    }

    // Check the response.
    if (resp == null) {
      throw new SOAPException(Constants.FAULT_CODE_CLIENT, "Response object is null !");
    }
    if (resp.generatedFault()) {
      Fault fault = resp.getFault();
      throw new SOAPException(fault.getFaultCode(), fault.getFaultString());
    }

    try {
      return manageResponse(resp);
    }
    catch (IOException ex) {
      throw new SOAPException(Constants.FAULT_CODE_CLIENT, "Difficulty to read from data source!", ex);
    }
  }

  /**
   * builds the xml response as a String
   *
   * @param resp
   * @return
   * @throws IOException
   * @throws SOAPException
   * @throws UnsupportedEncodingException
   */
  private String manageResponse(Response resp) throws IOException, SOAPException,
      UnsupportedEncodingException {
    Parameter retValue = resp.getReturnValue();
    DataHandler odh = (DataHandler)retValue.getValue();
    InputStream is = odh.getInputStream();
    int available = is.available();
    if (available < 1) {
      throw new SOAPException(Constants.FAULT_CODE_CLIENT, "Got an empty response (available bytes: " + available +
          ") !");
    }

    byte ba[] = new byte[available];
    // Apache BUG / have to double check byte array size (due to
    // potential
    // encoding/decoding of the HTTP stream, in this case is.available
    // returns a wrong number.
    int actual = is.read(ba);
    is.close();
    if (actual < 1) {
      throw new SOAPException(Constants.FAULT_CODE_CLIENT,
          "Response stream did not return any byte (available bytes: " + available + " ; read bytes: " + actual +
              ") !");
    }
    String response = null;
    String contentType = Constants.HEADERVAL_CONTENT_TYPE_UTF8;
    try {
      contentType = resp.getSOAPContext().getContentType();
    }
    catch (MessagingException e) {
      log.info("Could not read content type of response, considering charset utf-8. " + e.getMessage());
    }
    // If response is in utf 8 the build the string accordingly else leave default
    if (contentType != null && contentType.toLowerCase().contains(Dom4jUtils.DEFAULT_ENCODING)) {
      response = new String(ba, 0, actual, Dom4jUtils.DEFAULT_ENCODING);
    }
    else {
      response = new String(ba, 0, actual);
    }
    return response;
  }

  /**
   * returns the charset depending on the encoding attribute of the xml
   *
   * @param xmlContent
   * @return
   */
  private String getCharsetFromXml(String xmlContent) {
    String charset = Constants.HEADERVAL_CONTENT_TYPE_UTF8;
    if (Constants.HEADERVAL_DEFAULT_CHARSET.equalsIgnoreCase(EncodingUtils.getStringEncoding(xmlContent))) {
      charset = Constants.HEADERVAL_CONTENT_TYPE + ";charset=" + Constants.HEADERVAL_DEFAULT_CHARSET;
    }
    return charset;
  }

  /**
   * constructs an url with the eventual parameters like the JsessionId cookie.
   *
   * @return
   * @throws SOAPException
   */
  private URL buildUrl() throws SOAPException {
    URL tmpUrl = null;
    String url = null;
    try {
      if (sessionCookie != null && !isSessionLess) {
        url = addParamToUrl(requestUrl, sessionCookie);
      }
      else {
        url = requestUrl;
      }
      url = appendAdditionalParamsToUrl(url);
      tmpUrl = new URL(url);
    }
    catch (MalformedURLException ex) {
      log.error("Malformed URL ", ex);
      throw new SOAPException(Constants.FAULT_CODE_CLIENT, "Malformed URL error !", ex);
    }
    return tmpUrl;
  }

  /**
   * Adds parameters to the url
   *
   * @param tmpUrl
   * @return
   */
  protected String appendAdditionalParamsToUrl(String tmpUrl) {
    String params = ConfigurationParameters.getUrlAdditionalParams();
    if (params != null) {
      log.info("Appending parameters to url.");
      log.info(tmpUrl);
      tmpUrl = addParamToUrl(tmpUrl, params);
      log.info("=> " + tmpUrl);
    }
    return tmpUrl;
  }

  /**
   * Adds a parameter to the url
   *
   * @return
   */
  protected String addParamToUrl(String url, String param) {
    String separator = "?";
    if (url.contains(separator)) {
      separator = "&";
    }
    return url + separator + param;
  }

  /**
   * Invoke SOAP service. Send the request to the RPC service payload is part of
   * the SOAP enveloppe.
   *
   */
  private String processPayload(Call call, String reqparam) throws SOAPException {
    // configure parameter vector
    List<Parameter> params = new ArrayList<Parameter>();
    Parameter nameParam = new Parameter("request", java.lang.String.class, reqparam, null);
    params.add(nameParam);
    call.setParams(new Vector<Parameter>(params));

    // Finally invoke the SOAP service on server

    /*
     * CR 2331331 Some services (MSDN ones for instance) are not compatible with
     * SOAP-RPC. Since we do not use the acknowledgement, we will let parsing
     * Exceptions through using the fact that having them means that we got an
     * answer from the client. Note that this is possible because we do nothing
     * with the acknowledgement.
     *
     * @since version 0.2
     */
    Response resp = null;
    try {
      resp = call.invoke(servletUrl, mSOAPAction);
    }
    catch (SOAPException soapex) {
      if (soapex.getTargetException() instanceof IllegalArgumentException) {
        // Building a Fake response
        resp = new Response(servletUrl.toString(), "FakeMethodName", new Parameter("fakeReturn", String.class,
            "FakeOK",
            Constants.NS_URI_SOAP_ENC), null, null, Constants.NS_URI_SOAP_ENC, null);
      }
      else {
        throw soapex;
      }
    }
    // Retrieves cookie if neccessary
    if (sessionCookie == null) {
      try {
        Hashtable<String, String> ht = m_soapConnection.getHeaders();
        if (ht != null) {
          sessionCookie = ht.get(SET_COOKIE);
        }
      }
      catch (Exception e) {
      }
    }

    // Check the response.
    if (resp == null) {
      throw new SOAPException(Constants.FAULT_CODE_CLIENT, "Response object is null !");
    }
    if (resp.generatedFault()) {
      Fault fault = resp.getFault();
      throw new SOAPException(fault.getFaultCode(), fault.getFaultString());
    }
    return (String)resp.getReturnValue().getValue();
  }

  // Serialization related methods
  private void writeObject(java.io.ObjectOutputStream out) throws IOException {
    out.writeBoolean(useAttachment);
    out.writeObject(sessionCookie);
    out.writeObject(mSOAPAction);
    out.writeObject(requestUrl);
    out.writeInt(sessionTimeout);
    out.writeObject(urnService);
    out.flush();
  }

  private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
    useAttachment = in.readBoolean();
    sessionCookie = (String)in.readObject();
    mSOAPAction = (String)in.readObject();
    requestUrl = (String)in.readObject();
    sessionTimeout = in.readInt();
    urnService = (String)in.readObject();

    // terminate object init :
    servletUrl = new URL(requestUrl);
    m_soapConnection = new SOAPHTTPConnection();
    m_soapConnection.setMaintainSession(!isSessionLess);
    // m_serialization = true;
  }

  public void setSessionLess(boolean isSessionLess) {
    this.isSessionLess = isSessionLess;
    if (m_soapConnection != null) {
      m_soapConnection.setMaintainSession(!isSessionLess);
    }
  }

}
